/* ************************************************************************ */
/*                                                                          */
/*  kyototycoon-neko														*/
/*  HaXe/neko Library													    */
/*  Copyright (c)2011 François Nicaise                                      */
/*                                                                          */
/*  This program is free software; you can redistribute it and/or modify    */
/*  it under the terms of the GNU General Public License as published by    */
/*  the Free Software Foundation; either version 2 of the License, or       */
/*  (at your option) any later version.                                     */
/*                                                                          */
/*  This program is distributed in the hope that it will be useful,         */
/*  but WITHOUT ANY WARRANTY; without even the implied warranty of          */
/*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           */
/*  GNU General Public License for more details.                            */
/*                                                                          */
/*  You should have received a copy of the GNU General Public License       */
/*  along with this program; if not, write to the Free Software             */
/*  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */
/*                                                                          */
/* ************************************************************************ */

package kt;

import Type;
import Reflect;
import kt.RemoteDB;

class Manager< T : Object > {

	public static var cnx(default,setConnection) : RemoteDB;

	private static var object_cache : Hash<Dynamic> = new Hash();
	private static var cache_field = "__cache__";

	public static function setConnection( c : RemoteDB ) {
		Reflect.setField(Manager,"cnx",c);
		return c;
	}

	var recordname : String;
	var class_proto : { prototype : Dynamic };
	var class_fields : List<String>;
	var cnameTokens : Int;
	var currentTarget : String; // target database
	var keyForced : Bool; // use user custom key

	public function new( classval : Class<T> ) {
		var cl : Dynamic = classval;
		class_proto = cl;	
		var cname : Array<String> = cl.__name__;
		cnameTokens = cname.length - 1;
		recordname = cname.join(":");

		// get the list of private fields
		var apriv : Array<String> = cl.PRIVATE_FIELDS;
		apriv = if( apriv == null ) new Array() else apriv.copy();
		apriv.push("local_manager");
		apriv.push("__class__");

		// get the proto fields not marked private (excluding methods)
		class_fields = new List();
		var proto : { local_manager : kt.Manager<T> } = class_proto.prototype;
		for( f in Reflect.fields(proto) ) {
			var isfield = !Reflect.isFunction(Reflect.field(proto,f));
			if( isfield )
				for( f2 in apriv )
					if( f == f2 ) {
						isfield = false;
						break;
					}
			if( isfield )
				class_fields.add(f);
		}

		proto.local_manager = this;
		currentTarget = null; // take the first database by default
	}

	public static function cleanup() {
		object_cache = new Hash();
	}

	function make( x : T ) {
	}

	function unmake( x : T ) {
	}

	/*------------------ Local Manager API ------------------*/
	
	function doSet( x : T ) {
		unmake(x);
		var data : Array<BulkRecord>= prepareBulk( x );
		cnx.setTarget( currentTarget );
		cnx.setBulkBinary( data );
		addToCache( x );
	}

	function doReplace( x : T ) {
		var key = untyped(x).key;
		if( key == null ) throw "Replace failed : record does not exist.";
		unmake(x);
		var data : Array<BulkRecord>= prepareBulk( x, true );
		cnx.setTarget( currentTarget );
		cnx.setBulkBinary( data );
		var cache = getFromCache(x);
		for( f in class_fields ) {
			var v = Reflect.field(x,f);
			var vc = Reflect.field(cache,f);
			if( v != vc ) Reflect.setField(cache,f,v);
		}
	}

	function doRemove( x : T ) : Bool {
		var key = untyped(x).key;
		if( key == null ) "Delete Failed : record does not exist.";
		var keys = prepareBulkKeys( key );
		cnx.setTarget( currentTarget );
		var deletedRecords = cnx.removeBulkBinary( keys );
		if( deletedRecords < 0 ) return false;
		removeFromCache(x);
		for( f in Reflect.fields(x) ){
			Reflect.deleteField(x,f);
		}
		x= null;
		return true;
	}


	/*------------------ Public API ------------------*/

	public function getConn( useTarget = true ) : kt.RemoteDB { 
		if( useTarget ) cnx.setTarget( currentTarget );
		return cnx;
	}

	public function clear() { 
		cnx.setTarget( currentTarget );
		cnx.clear();
	}

	public function setTarget( target : String ) {
		currentTarget = target;
	}

	// Use User key instead of internal one
	public function forceKey() {
		keyForced = true;
	}

	public function get( k_int : Int = -1, k_string : String = "" ) : T {
		var key = if( k_int == -1 ) k_string else k_int + ""; 

		// TODO : check if there aren't any `:` in the key !

		var x : Dynamic = getFromCacheKey( key );
		if( x != null ) {
			return x;
		}

		var keys = prepareBulkKeys( key );
		cnx.setTarget( currentTarget );
		var records = cnx.getBulkBinary( keys,null,currentTarget );
		if( records == null ) return null;
		var x : Dynamic = {};
		var key = null;
		for( f in class_fields ) {
			Reflect.setField(x,f,null);
		}
		for( r in records ) {
			var field_data = r.key.split(":");
			key = field_data[if( keyForced ) 0 else cnameTokens+1];
			var fname = field_data[if( keyForced ) 1 else cnameTokens+2];
			var v = if( r.value == "" ) null else r.value;
			if( v == null ) continue;
			if( v.length > 0 && v.charAt(0) == "~" ) v  = haxe.Unserializer.run(v.substr(1,v.length-1));
			Reflect.setField(x,fname, v);
		}
		Reflect.setField(x,"key", if( keyForced ) key else makeKey(key));
		cacheObject( x );
		make(x);
		return x;
	}


	/*------------------ Private helper functions ------------------*/

	private function prepareBulkKeys( key : String ) {
		var a : Array<String>= new Array();
		for( f in class_fields ) {
			a.push( makeField(f, key) );
		}
		return a;
	}

	private function prepareBulk( x : T, replace = false ) {
		var a : Array<BulkRecord>= new Array();

		var xx = untyped (x);
		var providedKey = xx.key;
		var xt = xx.xt;
		cnx.setTarget( currentTarget );
		var key = if( providedKey == null ) cnx.increment( dbIndex(x) , 1 ) + "" else providedKey;
		untyped(x).key = key;
		
//		var cache = getFromCache(x);
		Reflect.field(x,cache_field);	
		//var cache = Reflect.field(x,cache_field);
		for( f in class_fields ) {
			var v = Reflect.field( x, f );
			var type : ValueType =  Type.typeof(v);
			var val : String = if( v == null ) "" else 
				switch( type ) {
					case TNull : "";
					case TInt : Std.string(v);
					case TFloat : Std.string(v);
					case TBool : Std.string(v);
					case TClass(Dynamic) : 
						if( Type.enumEq( type, TClass(String ) ) ) 
							v + ""
						else if( Type.enumEq( type, TClass(Date) ) ) {
							var d : Date = cast v;
							"~" + haxe.Serializer.run( d );
						}
					default : "~" + haxe.Serializer.run( v );
				};

			var idx = cnx.getDbIdx(currentTarget);
			var key = if( replace ) key+ ":" + f else makeField( f, key);
			var br = {key: key, 
				value: val, 
				dbidx: idx, 
				expirationTime : if( xt == null ) 0 else xt };
			a.push( br );
		}

		return a;
	}
	
	private function makeField( f , key ): String {
		return if( keyForced ) key + ":" + f else recordname + ":" + key + ":" + f;
	}

	private function dbIndex( x : T ) {
		return recordname + "_autoidx";
	}

	private function prepareKey( x : T = null ) {
		var xx = untyped (x);
		var providedKey = xx.key;
		return makeKey( providedKey ) ;
	}

	function makeKey( key : String ) {
		return if( keyForced ) key else recordname + ":" + key;
	}

	/* ---------------------------- OBJECT CACHE -------------------------- */

	function cacheObject( x : T ) {
		addToCache(x);
		untyped __dollar__objsetproto(x,class_proto.prototype);
//		Reflect.setField(x,cache_field,untyped __dollar__new(x));
	}

	function addToCache( x : T ) {
		object_cache.set(prepareKey(x),x);
	}

	function removeFromCache( x : T ) {
		object_cache.remove(prepareKey(x));
	}

	function getFromCacheKey( providedKey : String ) : T {
		var key = if( keyForced ) providedKey else recordname + ":" + providedKey;
		var c : Dynamic = cast object_cache.get(key);
		if( c != null ) make( c);
		return c;
	}

	function getFromCache( x : T ) : T {
		var c : Dynamic = cast object_cache.get(prepareKey(x));
		return c;
	}
	
}
